import math
import sys
import json
import time

from collections import deque
from absl import app, flags


from cc.appcoin2.strategy.py_strat.strategy_multiprocessing import StrategyBase, PyProduct
from cc.appcoin2.strategy.py_strat.util.account_manager import AccountManager
from cc.appcoin2.strategy.py_strat.util.options_pricer_helper import *


class FuturesPricer:
  def __init__(self, window_size: int, trade_symbol, ref_symbol, edge):
    self._window_size = window_size
    self._trade_symbol = trade_symbol
    self._ref_symbol = ref_symbol
    self._edge = edge * 1e-4
    self._price = [0,0]
    self._count = [0,0]
    self._deque = deque()

  def update_by_book(self, book):
    symbol = "{}:{}:{}".format(book.market_str, book.exchange_str, book.relative_norm)
    if symbol not in [self._trade_symbol, self._ref_symbol]:
      return
    if book.ask0() is None or book.bid0() is None:
      return

    if symbol == self._trade_symbol:
      assert book.ask0() is not None
      assert book.bid0() is not None
      self._price[0] = 0.5 * (book.ask0().price + book.bid0().price)
      self._count[0] += 1
    else:
      assert book.ask0() is not None
      assert book.bid0() is not None
      self._price[1] = 0.5 * (book.ask0().price + book.bid0().price)
      self._count[1] += 1

    if all(c > 10 for c in self._count):
      self.update(book.timestamp, math.log(self._price[0] / self._price[1]))

  def update(self, ts, x):
    self._deque.append((ts, x))
    while len(self._deque) > 0 and ts - self._deque[0][0] > self._window_size:
      self._deque.popleft()

  def get_average(self):
    return sum(d[1] for d in self._deque)/len(self._deque)

  def is_ready(self):
    return all(c > 100 for c in self._count)

  def calc_price(self, book):
    symbol = "{}:{}:{}".format(book.market_str, book.exchange_str, book.relative_norm)
    if symbol != self._trade_symbol:
      return

    avg = self.get_average()
    ref_price = 0.5 * (book.ask0().price + book.bid0().price)
    askp = math.exp(avg + self._edge) * ref_price
    bidp = math.exp(avg - self._edge) * ref_price
    return [askp, bidp]


class OptionsPricer:
  def __init__(self, window_size: int, trade_symbol, ref_symbol, edge):
    self._window_size = window_size
    self._trade_symbol = trade_symbol
    self._ref_symbol = ref_symbol
    self._edge = edge * 1e-4

    self._shared_value = SharedValue()

  def update_by_book(self, book):
    if book.ask0() is None:
      return
    if book.bid0() is None:
      return
    if (len(book.absolute_norm.split('.'))) < 3:
      #update forward price
      self._shared_value.set_forward_value(0.5 * (book.ask0().price + book.bid0().price))
      return
    elif (book.market_str == "Options" and self._shared_value.get_forward_value() is not None):
      #update historical data
      s_t = time.strptime(book.absolute_norm.split('.')[1], "%Y%m%d")
      mkt = int(time.mktime(s_t)) + 28800
      strike_price = float(book.relative_norm.split('.')[3])
      cp_flag = book.relative_norm.split('.')[2]
      T = (mkt - book.timestamp / 1e9) / 31536000.
      bid_iv = MathematicalTools.find_vol(book.bid0().price, cp_flag, 1., strike_price / self._shared_value.get_forward_value(), T)
      ask_iv = MathematicalTools.find_vol(book.ask0().price, cp_flag, 1., strike_price / self._shared_value.get_forward_value(), T)
      if bid_iv is not None and ask_iv is not None:
        self._shared_value.update_bid_historical_data(book.absolute_norm,
            [mkt, book.timestamp / 1e9, self._shared_value.get_forward_value(), strike_price, bid_iv])
        self._shared_value.update_ask_historical_data(book.absolute_norm,
            [mkt, book.timestamp / 1e9, self._shared_value.get_forward_value(), strike_price, ask_iv])

  def update(self, ts, x):
    pass

  def get_average(self):
    pass

  def is_ready(self):
    if not self._shared_value._bid_queue.empty():
      result = self._shared_value._bid_queue.get()
      if result is not None:
        self._shared_value._bid_params = result[0]
        self._shared_value._bid_ga.pop_position = result[1]
    if not self._shared_value._ask_queue.empty():
      result = self._shared_value._ask_queue.get()
      if result is not None:
        self._shared_value._ask_params = result[0]
        self._shared_value._ask_ga.pop_position = result[1]
    return (self._shared_value._bid_params is not None and self._shared_value._ask_params is not None
            and self._shared_value.get_forward_value() is not None)

  def calc_price(self, book):
    if (len(book.absolute_norm.split('.'))) < 3:
      return None

    s_t = time.strptime(book.absolute_norm.split('.')[1], "%Y%m%d")
    mkt = int(time.mktime(s_t))
    T = (mkt+28800 - book.timestamp / 1e9) / 31536000.
    strike_prices = float(book.absolute_norm.split('.')[3])
    cp_flag = book.absolute_norm.split('.')[2]

    bid_params = self._shared_value._bid_params
    bid_iv = MathematicalTools.sabr_model(bid_params[0], bid_params[1], bid_params[2], bid_params[3],
                                          T, self._shared_value.get_forward_value(), strike_prices)
    bidp = MathematicalTools.bs_price(cp_flag, 1., strike_prices / self._shared_value.get_forward_value(), T, bid_iv)
    ask_params = self._shared_value._ask_params
    ask_iv = MathematicalTools.sabr_model(ask_params[0], ask_params[1], ask_params[2], ask_params[3],
                                          T, self._shared_value.get_forward_value(), strike_prices)
    askp = MathematicalTools.bs_price(cp_flag, 1., strike_prices / self._shared_value.get_forward_value(), T, ask_iv)


    ask0 = book.ask0().price if book.ask0() is not None else 0
    bid0 = book.bid0().price if book.bid0() is not None else 0
    print("{}:{}:{} (ask0: {} bid0: {}) (askp: {} bidp: {})".format(book.market_str,
                                                                    book.exchange_str,
                                                                    book.absolute_norm,
                                                                    ask0,
                                                                    bid0,
                                                                    askp,
                                                                    bidp))
    askp = max(askp, ask0) if ask0 > 0 else askp
    bidp = min(bidp, bid0) if bid0 > 0 else bidp

    return [askp, bidp]


class ProductMmStrategy:
  def __init__(self, simple_mm_strategy, strat_config: dict):
    self._simple_mm_strategy = simple_mm_strategy
    self.init_with_config(strat_config)
    #self._live_orders = dict()
    self._last_submit_order_ns = None
    self._last_cancel_order_ns = None


  def init_with_config(self, strat_config: dict):
    self._og_name = strat_config['og_name']
    self._trade_symbol = strat_config['trade_symbol']
    assert len(strat_config['refer_symbol']) > 0
    self._refer_symbol = strat_config['refer_symbol'][0]
    self._lot_size = strat_config['lot_size']
    self._now_pos = None
    self._min_pos = strat_config['min_pos']
    self._max_pos = strat_config['max_pos']
    self._edge_bp = strat_config['edge_bp']
    self._window_size_ns = strat_config['basis_ma_window'] * 1e9
    self._max_posting_period_ns = int(strat_config['max_posting_period_sec']) * 1e9
    self._submit_order_cooldown_ns = self._max_posting_period_ns
    self._cancel_order_cooldown_ns = 5 * 1e9 # 5s
    items = self._trade_symbol.split(':')
    self._market = items[0]
    self._exchange = items[1]
    self._symbol_str = items[2]
    product = PyProduct.from_norm(self._trade_symbol, time.time_ns())
    self._absolute_norm = "{}:{}:{}".format(self._market, self._exchange, product.absolute_norm) # for accounting
    assert self._og_name
    assert self._trade_symbol
    assert self._refer_symbol
    assert self._max_posting_period_ns
    if 'Options' in self._trade_symbol:
      self._pricer = OptionsPricer(self._window_size_ns,
                                   self._trade_symbol,
                                   self._refer_symbol,
                                   self._edge_bp)
    else:
      self._pricer = FuturesPricer(self._window_size_ns,
                                   self._trade_symbol,
                                   self._refer_symbol,
                                   self._edge_bp)
    print("create product strategy for {}, refer to {}".format(self._trade_symbol,
        self._refer_symbol))

  def manage_order(self, timestamp, ask_price, bid_price):
    fire_ask = True
    fire_bid = True

    if self._now_pos is None:
      return
    if self._last_submit_order_ns is not None and \
        timestamp < self._last_submit_order_ns + self._submit_order_cooldown_ns:
      return

    ask_qty = min(self._lot_size, self._now_pos - self._min_pos)
    bid_qty = min(self._lot_size, self._max_pos - self._now_pos)

    if ask_price <= 0 or ask_qty <= 0:
      fire_ask = False
    if bid_price <= 0 or bid_qty <= 0:
      fire_bid = False

    if self._now_pos is None or self._now_pos <= self._min_pos:
      fire_ask = False
    if self._now_pos is None or self._now_pos >= self._max_pos:
      fire_bid = False

    me = "{}:{}".format(self._market, self._exchange)
    working_orders = self._simple_mm_strategy.get_all_working_orders(me)
    for order in working_orders:
      # TODO(daniel): update absolute_norm & relative_norm for order dict
      symbol = "{}:{}:{}".format(order['market'], order['exchange'], order['absolute_norm'])
      if symbol not in [self._trade_symbol, self._absolute_norm]:
        continue
      order_side = order['order_side']
      if order_side == 'SELL_ORDER':
        fire_ask = False
      if order_side == 'BUY_ORDER':
        fire_bid = False

    if fire_ask:
      # sell
      self._last_submit_order_ns = timestamp
      self.submit_order(
        timestamp=timestamp,
        norm_symbol=self._trade_symbol,
        order_price=float(ask_price),
        order_qty=float(ask_qty),
        order_side="SELL_ORDER",
        order_type="LIMIT_ORDER",
        order_duration="GTC_ORDER",
        post_only=True,
        reduce_only=False,
        tag="")

    if fire_bid:
      # buy
      self._last_submit_order_ns = timestamp
      self.submit_order(
        timestamp=timestamp,
        norm_symbol=self._trade_symbol,
        order_price=float(bid_price),
        order_qty=float(bid_qty),
        order_side="BUY_ORDER",
        order_type="LIMIT_ORDER",
        order_duration="GTC_ORDER",
        post_only=True,
        reduce_only=False,
        tag="")

  def manage_risks(self, timestamp):
    if self._last_cancel_order_ns is not None and \
        timestamp < self._last_cancel_order_ns + self._cancel_order_cooldown_ns:
      return
    self._last_cancel_order_ns = timestamp

    ask_working_orders = []
    bid_working_orders = []
    working_orders = self._simple_mm_strategy.get_all_working_orders()
    for order in working_orders:
      if 'external_order_id' not in order:
        continue
      order_id = order['external_order_id']
      if not order_id: # not accepted
        continue
      # TODO(daniel): update absolute_norm & relative_norm for order dict
      symbol = "{}:{}:{}".format(order['market'], order['exchange'], order['absolute_norm'])
      if symbol not in [self._trade_symbol, self._absolute_norm]:
        continue
      order_side = order['order_side']
      if "SELL_ORDER" == order_side:
        ask_working_orders.append(order)
      elif "BUY_ORDER" == order_side:
        bid_working_orders.append(order)
      else:
        print("Error: unknown order side: {}".format(order))
        assert False

    # cancel sell order
    for order in ask_working_orders:
      if order['order_created_time'] + self._max_posting_period_ns > time.time_ns():
        continue
      self.cancel_order(order['proc_order_id'])

    # cancel buy order
    for order in bid_working_orders:
      if order['order_created_time'] + self._max_posting_period_ns > time.time_ns():
        continue
      self.cancel_order(order['proc_order_id'])

  def submit_order(self,
                   timestamp: int,
                   norm_symbol: str,
                   order_price: float,
                   order_qty: float,
                   order_side: str,
                   order_type: str,
                   order_duration: str,
                   post_only: bool,
                   reduce_only: bool,
                   tag: str):
    product = PyProduct.from_norm(norm_symbol, timestamp)
    order_spec = {
      "market_type": product.market_name,
      "exchange_type": product.exchange_name,
      "product": product.native_symbol,
      "norm_product": product.relative_norm,
      "order_price": order_price,
      "order_qty": order_qty,
      "order_side": order_side,
      "order_type": order_type,
      "order_duration": order_duration,
      "timestamp": timestamp,
      "post_only": post_only,
      "reduce_only": False,
      "leverage_rate": 1,
      "tag": tag,
    }
    if product.market_name in ['Futures', 'Options']:
      order_spec['reduce_only'] = reduce_only

    exec_spec = {
      "exec_type": "SUBMIT_ORDER",
      "og_name": self._og_name,
      "order_spec": order_spec,
    }
    #print(exec_spec)
    self._simple_mm_strategy.submit_order(exec_spec)

  def cancel_order(self, proc_order_id: int):
    exec_spec = {
      "exec_type": "CANCEL_ORDER",
      "og_name": self._og_name,
      "proc_order_id": proc_order_id,
    }
    #print(exec_spec)
    self._simple_mm_strategy.cancel_order(exec_spec)

  def on_book_feed(self, book):
    self._pricer.update_by_book(book)
    if not self._pricer.is_ready():
      return

    prices = self._pricer.calc_price(book)
    if prices is None:
      return

    symbol = "{}:{}:{}".format(book.market_str, book.exchange_str, book.absolute_norm)
    if symbol not in [self._absolute_norm]:
      return

    self.manage_order(book.timestamp, prices[0], prices[1])
    self.manage_risks(book.timestamp)

  def on_trade_feed(self, trade):
    pass

  def on_account_order(self, order_upd):
    pass

  def on_account_info(self, account_info):
    info = account_info.as_dict()
    if info['market'] not in [self._market]:
      return
    if info['exchange'] not in [self._exchange]:
      return
    self.update_balance(info)
    self.update_position(info)

  def update_balance(self, info):
    market = info['market']
    if market not in ['Spot']:
      return
    if 'account_balance' not in info:
      return
    if 'each_balance' not in info['account_balance']:
      return
    exchange = info['exchange']
    for balance in info['account_balance']['each_balance']:
      pass # TODO:

  def update_position(self, info):
    market = info['market']
    if market not in ['Futures', 'Options']:
      return
    if 'account_position' not in info:
      return
    if 'each_position' not in info['account_position']:
      return
    exchange = info['exchange']
    now_pos = None
    for position in info['account_position']['each_position']:
      symbol = "{}:{}:{}".format(market, exchange, position['symbol'])
      if symbol != self._absolute_norm:
        continue
      now_pos = position['net_position']
      break
    self._now_pos = now_pos

  def print_strategy_status(self):
    print("current strategy status:\ntrade_symbol: {}\nnow_pos: {}\nmin_pos: {}\nmax_pos: {}".format(
        self._trade_symbol,
        self._now_pos,
        self._min_pos,
        self._max_pos))


class SimpleMmStrategy(StrategyBase):
  def __init__(self, driver_config, strat_config):
    super().__init__(driver_config)
    self.init_with_config(driver_config, strat_config)
    self.redirect_cpp_log_to_file(flags.FLAGS.cpp_log_path, True)
    self.redirect_py_log_to_file(flags.FLAGS.py_log_path, True)

    self._account_manager = AccountManager()

    self._last_print_status_ns = None
    self._last_position_update_ts = None

  def init_with_config(self, driver_json, config_json):
    self._product_strategy_list = []
    self._product_strategy_dict = {}
    # load driver
    with open(driver_json) as f:
      self._driver = json.load(f)
    os_confs = self._driver['order']
    exchanges = os_confs['exchanges']
    og_mea_dict = {}
    for key, val in exchanges.items():
      if key in og_mea_dict.keys():
        raise "dupplicated og name configured"
      og_mea_dict[key] = val['mea']
      self._og_name_list.append(key)
    # load config
    with open(config_json) as f:
      config = json.load(f)
    products = config['products']
    mea_outer = config['mea']

    def get_og_name_by_mea(og_mea_dict, mea):
      # need to update if there are 2 OGs with same mea
      og_name = None
      for key, val in og_mea_dict.items():
        if val == mea:
          og_name = key
          break
      return og_name

    print("initialize product strategy: start")
    for product in products.values():  # create product strategy
      trade_symbol = product['trade_symbol']
      refer_symbol = []
      for symbol in product['ref_symbol']:
        refer_symbol.append(symbol)
      mea_inner = mea_outer if 'mea' not in product else product['mea']
      strat_conf = {
          "og_name": get_og_name_by_mea(og_mea_dict, mea_inner),
          "trade_symbol": trade_symbol,
          "refer_symbol": refer_symbol,
          "basis_ma_window": product['basis_ma_window'],
          "lot_size": product['lot_size'],
          "min_pos": product['min_pos'],
          "max_pos": product['max_pos'],
          "edge_bp": product['edge_bp'],
          "max_posting_period_sec": product['max_posting_period_sec'],
      }
      product_strategy = ProductMmStrategy(self, strat_conf)
      self._product_strategy_list.append(product_strategy)
      for symbol in refer_symbol + [trade_symbol]:
        # for deribit options, as there are multiple relative_norm map to
        # same native_symbol, so use absolute_norm as product key first
        product = PyProduct.from_norm(symbol, time.time_ns())
        product_key = "{}:{}:{}".format(product.market_name,
                                        product.exchange_name,
                                        product.absolute_norm)
        if product_key in self._product_strategy_dict:
          self._product_strategy_dict[product_key].append(product_strategy)
        else:
          self._product_strategy_dict[product_key] = [product_strategy]
    print("initialize product strategy: done")

  def is_valid_feed(self, feed):
    now = time.time_ns()
    if feed.timestamp + 10000000 < now: # feed handle delay tolerance 10ms
      return False
    else:
      return True

  def on_book_feed(self, book):
    #jd = book.as_dict()
    #print(json.dumps(jd, indent=2))

    # example
    # if self._last_position_update_ts and self._last_position_update_ts + 1e10 < book.timestamp:
    #   print("Long Time No Position Update!")

    if not self.is_valid_feed(book):
      return
    try:
      symbol = "{}:{}:{}".format(book.market_str, book.exchange_str, book.absolute_norm)
      if symbol not in self._product_strategy_dict.keys():
        return
      product_strategy_list = self._product_strategy_dict[symbol]
      for strat in product_strategy_list:
        strat.on_book_feed(book)
    except Exception as exc:
      import traceback
      print(traceback.format_exc())
      raise

  def on_trade_feed(self, trade):
    #jd = trade.as_dict()
    #print(json.dumps(jd, indent=2))
    if not self.is_valid_feed(trade):
      return
    try:
      symbol = "{}:{}:{}".format(trade.market_str, trade.exchange_str, trade.absolute_norm)
      if symbol not in self._product_strategy_dict.keys():
        return
      product_strategy_list = self._product_strategy_dict[symbol]
      for strat in product_strategy_list:
        strat.on_trade_feed(trade)
    except Exception as exc:
      import traceback
      print(traceback.format_exc())
      raise

  def on_account_order(self, order_upd):
    upd = order_upd.as_dict()
    print(json.dumps(upd, indent=2))
    try:
      symbol = "{}:{}:{}".format(order_upd.market_str, order_upd.exchange_str, order_upd.absolute_norm)
      if symbol not in self._product_strategy_dict.keys():
        return
      product_strategy_list = self._product_strategy_dict[symbol]
      for strat in product_strategy_list:
        strat.on_account_order(order_upd)
    except Exception as exc:
      import traceback
      print(traceback.format_exc())
      raise

  def on_account_info(self, account_info):
    self._account_manager.on_account_info(account_info)  # update local account manager
    info = account_info.as_dict()
    if account_info.is_position_update:
      self._last_position_update_ts = account_info.timestamp
    try:
      for strat in self._product_strategy_list:
        strat.on_account_info(account_info)
    except Exception as exc:
      import traceback
      print(traceback.format_exc())
      raise
    timestamp = info['timestamp']
    if self._last_print_status_ns is not None and timestamp < self._last_print_status_ns + 10e9:
      return
    self._last_print_status_ns = timestamp

    #account_status = self._account_manager.get_account_status()
    #if account_status:
    #  for meo, bal_pos in account_status.items():
    #    print('****************************************')
    #    print(meo)
    #    print('########################')
    #    print('BALANCE:')
    #    if 'balance' in bal_pos.keys():
    #      for item in bal_pos['balance']:
    #        print("{}: {}".format(item[0], item[1]))
    #    print('########################')
    #    print('POSITION:')
    #    if 'position' in bal_pos.keys():
    #      for item in bal_pos['position']:
    #        print("{}: {}".format(item[0], item[1]))
    #    print('########################')
    #    print('****************************************')

    for strat in self._product_strategy_list:
      strat.print_strategy_status()

  def on_account_risk(self, account_risk):
    #if account_risk.market != "Options":
    #  return;
    if not account_risk.is_undetected_position:
      return
    positions = account_risk.as_dict()
    for pos in positions['risk_undetect_position']:
      symbol_str = "{}:{}".format(account_risk.mea.replace(".", ":"), pos['native_symbol'])
      product = PyProduct.from_native(symbol_str, time.time_ns())
      product.search_product_holder(0);
      print("undetected position: \n{}".format(pos))


def main(argv):
  assert flags.FLAGS.driver
  assert flags.FLAGS.config
  strategy = SimpleMmStrategy(driver_config=flags.FLAGS.driver, strat_config=flags.FLAGS.config)
  strategy.run()


if __name__ == "__main__":
  flags.DEFINE_string('driver', None, 'driver config file.')
  flags.DEFINE_string('config', None, 'strategy config file.')
  flags.DEFINE_string('cpp_log_path', None, 'cpp log path.')
  flags.DEFINE_string('py_log_path', None, 'py log path.')
  app.run(main)
